关于std::function,几个行之有效的扩展小技巧
The following article is from CPP编程客 Author cpluspluser
开发中,若你的项目稍微具有点扩展性和灵活性,那便少不了会用到std::function。
std::function可以容纳任何形式的可调用体,比如普通函数,成员函数,Lambda 函数。
因此,可以借其来实现两个重要的功能:接口分离和时间分离。
接口分离指的是调用者和被调用者之间彼此分离,以降低二者的依存性。具体来说,你可以将任何可调用体保存到std::function中,可调用体不知道std::function的存在,反之亦如此。于是,可以做什么呢?将具体的处理方式等到用的时候再进行指定,调用者通过std::function这个桥梁,以这个随后指定的方式来处理实际的工作。
那时间分离有什么用呢?普通的函数,当你得到了实际数据,便可通过参数进行调用,也就是说得到数据的同时也就满足了调用的条件。在这里,函数的调用和满足的条件是紧密相连的。时间分离就是将这两部分分离,把调用的函数先保存起来,待条件满足之时再进行调用。
总而言之,通过接口分离和时间分离,可以降低模块之间的耦合,使程序更加具有可扩展性,使用起来会更加灵活。
正因如此,在过去的许多文章中,我们多次使用std::function来完成库的某些功能设计。
然而,有时你需要知道待调用函数的数据个数,也就是参数个数,甚至具体某位参数的类型。
亦或是,当你使用std::bind产生一个可调用体的时候,想要隐藏那些烦人的placeholders。
如何实现这些需求呢?
2 解决方案与实现
我们可以通过TMP来扩展std::function,添加一些小巧轻便的辅助工具,来实现上述需求。
建立一个function_traits模板类,用于萃取需要从std::function中获取的信息,代码如下:
1template<typename T>
2struct function_traits;
3
4template<typename R, typename... Args>
5struct function_traits<std::function<R(Args...)>>
6{
7 static constexpr std::size_t value = sizeof...(Args);
8 using result_type = R;
9
10 template<size_t I>
11 struct get {
12 using type = typename std::tuple_element<I, std::tuple<Args...>>::type;
13 };
14};
其中,通过sizeof...得到std::function中参数包的大小,存到value中。
对于参数包中的每个具体类型,如何操作呢?
我们讲过,对于类型操作的强大工具是TypeList,而std::tuple就是标准中TypeList的实现。所以借助std::tuple提供的索引式访问,想要获取具体某位的参数类型也不是什么难事。
来个使用的小例子:
1typedef std::function<void(int, double, std::string)> FuncType;
2std::cout << mc::function_traits<FuncType>::value << std::endl;
3std::cout << typeid(mc::function_traits<FuncType>::result_type).name() << std::endl;
4std::cout << typeid(mc::function_traits<FuncType>::get<0>::type).name() << std::endl;
5std::cout << typeid(mc::function_traits<FuncType>::get<1>::type).name() << std::endl;
6std::cout << typeid(mc::function_traits<FuncType>::get<2>::type).name() << std::endl;
7
8// Outputs:
9// 3
10// void
11// int
12// double
13// class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >
解决了std::function类型信息的问题,接着来看如何对std::bind的调用进行简化。
简化std::bind的核心问题在于如何自动填充placeholders,总的来说,有两种方法。
第一种是将所有placeholders的类型保存到std::tuple中,那么经由索引式访问,我们就可以得到具体某位的placeholder对象。
实现如下:
1using PlaceholdersList = std::tuple<decltype(std::placeholders::_1),
2 decltype(std::placeholders::_2),
3 decltype(std::placeholders::_3),
4 decltype(std::placeholders::_4),
5 decltype(std::placeholders::_5),
6 decltype(std::placeholders::_6),
7 decltype(std::placeholders::_7),
8 decltype(std::placeholders::_8),
9 decltype(std::placeholders::_9),
10 decltype(std::placeholders::_10),
11 decltype(std::placeholders::_11),
12 decltype(std::placeholders::_12),
13 decltype(std::placeholders::_13),
14 decltype(std::placeholders::_14),
15 decltype(std::placeholders::_15),
16 decltype(std::placeholders::_16),
17 decltype(std::placeholders::_17),
18 decltype(std::placeholders::_18),
19 decltype(std::placeholders::_19),
20 decltype(std::placeholders::_20)>;
21
22template<std::size_t... Is, typename F, typename... Args>
23auto bind_helper(std::index_sequence<Is...>, const F& f, Args&&... args)
24{
25 return std::bind(f, std::forward<Args>(args)...,
26 typename std::tuple_element<Is, PlaceholdersList>::type{}...);
27}
28
29
30template<typename FunctionType, typename F, typename... Args>
31auto binder(const F& f, Args&&... args)
32{
33 return bind_helper(std::make_index_sequence<
34 function_traits<FunctionType>::value>{}, f, std::forward<Args>(args)...);
35}
在这里就用到了前面实现的function_traits来获取具体的参数个数,再借助std::index_sequece,便可得到所需填充类型的索引。
知道了索引,也就可从PlaceholdersList得到具体的对象。
第二种方案是自定义placeholders,思路可以参考cppreference。
因为std::bind是依赖std::is_placeholder来判断一个类型是否是placeholder,所以可以通过特化std::is_placeholder来定义自己的placeholder类型。
实现如下:
1// the second method
2// user-defined placholder type.
3template<int N>
4struct MyPlaceholder {};
5
6namespace std {
7 template<int N>
8 struct is_placeholder<::MyPlaceholder<N>> : std::integral_constant<int, N> {};
9}
现在,需要在bind_helper中使用它来替换第一种方案:
1template<std::size_t... Is, typename F, typename... Args>
2auto bind_helper(std::index_sequence<Is...>, const F& f, Args&&... args)
3{
4 // use method 1
5 /*return std::bind(f, std::forward<Args>(args)...,
6 typename std::tuple_element<Is, PlaceholdersList>::type{}...);*/
7
8 // use method 2
9 return std::bind(f, std::forward<Args>(args)..., MyPlaceholder<Is + 1>{}...);
10}
显而易见,第二种方式要更加简洁,代码量更少,所以推荐使用这种方式。
该工具被我放在了mcevil库中,位于命名空间mc之下,具体代码可见:https://github.com/lkimuk/mcveil/blob/main/function_traits.hpp。
3 如何使用?借助上述工具优化okdp::subject接口
最后,来看看如何使用上述工具,来优化上节实现的泛型观察者okdp::subject接口。
再来回顾下其中的接口,具体可见文末的「相关文章」:
1template<typename ConcreteSubject>
2class subject : public ConcreteSubject {
3 // ...
4public:
5
6 Token attach(Target target) {
7 //auto token = std::make_shared<Target>(std::move(callback));
8 std::shared_ptr<Target> token(new Target(std::move(target)),
9 [&](Target* obj) { delete obj; this->cleanup(); }
10 );
11 observers_.push_back(token);
12 return token;
13 }
编写一段测试代码:
1struct Boss {
2 using ObserverType = std::function<void(const std::string&)>;
3};
4
5void print(const std::string& data) {
6 std::cout << __func__ << data << std::endl;
7}
8
9void update(const std::string& data) {
10 std::cout << __func__ << data << std::endl;
11}
12
13struct Foo {
14 void update(const std::string& obj) {
15 std::cout << "Foo::" << __func__ << obj << std::endl;
16 }
17};
18
19void TestFunc()
20{
21 okdp::subject<Boss> boss;
22 Foo foo;
23 auto token1 = boss.attach(&print);
24 auto token2 = boss.attach([&foo](const std::string& arg) { return foo.update(arg); });
25 auto token3 = boss.attach(std::bind(&Foo::update, foo, std::placeholders::_1);
26 //auto token4 = boss.attach(&Foo::update, foo);
27}
由于接口的定义形式,可以通过三种方式来进行调用,分别是直接传函数地址,Lambda形式和通过std::bind的形式。
但不论使用Lambda还是std::bind,调用之时要写的代码都很长,随着参数的增多,会极为不便。
此时,就可以借助上述实现的mc::binder来进行简化调用。
为okdp::subject添加一个重载版本的attach函数:
1template<typename F, typename T, typename... Args>
2Token attach(const F& f, T&& head, Args&&... args) {
3 return attach(mc::binder<Target>(f, std::forward<T>(head), std::forward<Args>(args)...));
4}
通过两个重载版本,例子中的四种调用方式就都可以支持,这将极大的提高程序的灵活性和易用性。
- EOF -
关注『CPP开发者』
看精选C++技术文章 . 加C++开发者专属圈子
点赞和在看就是最大的支持❤️